﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace Simple.HttpServer {
    public class FileResourceProvider : ResourceProvider {

        private string _modifiedPrefix;
        private string _baseUriAbsolutePath;

        public override void Initialize(ResourceProviderContext context) {
            base.Initialize(context);
            _modifiedPrefix = Regex.Replace(Context.Configuration.Prefix, "[+*]", Environment.MachineName).Trim('/');
            _baseUriAbsolutePath = string.Format("/{0}/", new Uri(_modifiedPrefix).AbsolutePath.Trim('/'));
        }

        public override ResourceData GetExceptionResource(Exception ex) {
            var result = new ResourceData();
            var stream = GetExceptionStream(ex);
            PrepareResult(result, stream, ex, HttpStatusCode.InternalServerError);
            return result;
        }

        public override ResourceData GetResource(Uri uri) {
            var result = new ResourceData();
            try {
                var localFile = ToLocalPath(uri);
                // If file is not inside base directory - shouldn't serve it
                var localPath = Path.GetFullPath(localFile);
                var basePath = Context.Configuration.BasePath;
                if (!localPath.StartsWith(basePath, StringComparison.OrdinalIgnoreCase)) {
                    PrepareResult(result, GetDirectoryListingDeniedStream(), new Exception("Directory browsing is denied"), HttpStatusCode.Forbidden);
                    return result;
                }

                // If evaluated localPath is not base path itself or subdirectory of it - deny access
                if (!localPath.StartsWith(basePath, StringComparison.OrdinalIgnoreCase)) {
                    PrepareResult(result, GetNotServedStream(), new Exception("Specified directory is not served"), HttpStatusCode.Forbidden);
                }

                var contentType = GetContentType(uri);
                result.ContentType = contentType;

                var shouldShowDirectoryContent = ShouldShowDirectoryContent(uri);
                if (shouldShowDirectoryContent) {
                    PrepareResult(result, GetDirectoryContentStream(localPath), null, HttpStatusCode.OK);
                    return result;
                }

                // Modify uri with the default file
                if (string.IsNullOrWhiteSpace(contentType)) {
                    PrepareResult(result, GetNotServedStream(), new Exception("Specified extension is not served"), HttpStatusCode.Forbidden);
                    return result;
                }

                // TODO: Use file extension in order to generate proper stream from uri. Currently only static file content will be used (file will be just loaded from file system without any processing)
                var fileStream = new FileStream(localFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
                PrepareResult(result, fileStream, null, HttpStatusCode.OK);
            } catch (FileNotFoundException fnfex) {
                PrepareResult(result, GetNotFoundStream(), fnfex, HttpStatusCode.NotFound);
            } catch (Exception ex) {
                PrepareResult(result, GetExceptionStream(ex), ex, HttpStatusCode.InternalServerError);
            }
            return result;
        }

        private void PrepareResult(ResourceData rd, Stream stream, Exception ex, HttpStatusCode statusCode) {
            rd.Exception = ex;
            if (ex != null) {
                rd.ContentType = "text/html";
            }
            rd.ContentLength = stream.Length;
            rd.Stream = stream;
            rd.StatusCode = statusCode;
        }

        private Stream GetNotFoundStream() {
            var result = GetSimpleHtmlErrorTextStream("Not found");
            return result;
        }

        private Stream GetExceptionStream(Exception ex) {
            var errorText = Context.Configuration.ShowFullException ? ex.ToString() : ex.GetType().ToString();
            var result = GetSimpleHtmlErrorTextStream(errorText);
            return result;
        }

        private Stream GetNotServedStream() {
            var result = GetSimpleHtmlErrorTextStream("The requested type is not served by this server");
            return result;
        }

        private string GetSimpleHtmlTextWithoutEncoding(string title, string style, string body) {
            var result = string.Format("<!DOCTYPE html><html><head><title>{0}</title>{1}</head><body>{2}</body></html>", title, style, body);
            return result;
        }

        private Stream GetDirectoryContentStream(string path) {
            var dirInfo = new DirectoryInfo(path);
            var dirs = dirInfo.GetDirectories();
            var files = dirInfo.GetFiles();
            var sb = new StringBuilder();
            var fileUri = new Uri(path);
            var relativePath = AppendBaseUriPath(fileUri.AbsolutePath.Substring(Context.Configuration.BasePath.Length));
            sb.AppendLine(string.Format("<h1>{0}</h1>", WebUtility.HtmlEncode(WebUtility.UrlDecode(relativePath))));
            sb.AppendLine("<table><tr><td>Date</td><td>Length</td><td>Name</td></tr>");
            foreach (var dir in dirs) {
                sb.AppendLine(CreateDirectoryRow(dir));
            }
            foreach (var file in files) {
                sb.AppendLine(CreateFileRow(file));
            }
            sb.Append("</table>");
            var style = "<style type=\"text/css\">table{border-collapse:collapse;}table, td, th{border:1px solid black; padding:8px;}</style>";
            var result = GetStream(GetSimpleHtmlTextWithoutEncoding("Directory listing", style, sb.ToString()));
            return result;
        }

        private string CreateDirectoryRow(DirectoryInfo dir) {
            var date = WebUtility.HtmlEncode(dir.CreationTime.ToString());
            var length = "0";
            var fileUri = new Uri(dir.FullName);
            var relativePath = AppendBaseUriPath(fileUri.AbsolutePath.Substring(Context.Configuration.BasePath.Length));
            var linkUrlEncoded = System.Security.SecurityElement.Escape(relativePath);
            var linkText = WebUtility.HtmlEncode(dir.Name);
            var link = string.Format("<a href=\"{0}\">{1}</a>", linkUrlEncoded, linkText);
            var result = string.Format("<tr><td>{0}</td><td>{1}</td><td>/{2}</td></tr>", date, length, link);
            return result;
        }

        private string CreateFileRow(FileInfo file) {
            var date = WebUtility.HtmlEncode(file.CreationTime.ToString());
            var length = file.Length.ToString();
            var fileUri = new Uri(file.FullName);
            var relativePath = AppendBaseUriPath(fileUri.AbsolutePath.Substring(Context.Configuration.BasePath.Length));
            var linkUrlEncoded = System.Security.SecurityElement.Escape(relativePath);
            var linkText = WebUtility.HtmlEncode(file.Name);
            var link = string.Format("<a href=\"{0}\">{1}</a>", linkUrlEncoded, linkText);
            var result = string.Format("<tr><td>{0}</td><td>{1}</td><td>/{2}</td></tr>", date, length, link);
            return result;
        }

        private string AppendBaseUriPath(string relativePath) {
            var result = _baseUriAbsolutePath + relativePath.Trim('/');
            return result;
        }

        private bool ShouldShowDirectoryContent(Uri uri) {
            bool result;
            if (!Context.Configuration.EnableDirectoryBrowsing) {
                result = false;
            } else {
                var extension = Path.GetExtension(uri.LocalPath);
                if (string.IsNullOrWhiteSpace(extension)) {
                    // Request contains a directory
                    result = true;
                } else {
                    // Request contains some file with extension
                    result = false;
                }
            }
            return result;
        }

        private Uri GetModifiedUriFromRequest(Uri uri) {
            Uri result;
            var localPath = uri.LocalPath;
            var extension = Path.GetExtension(localPath);
            if (!string.IsNullOrWhiteSpace(extension)) {
                // Extension was found - no modification is needed
                result = uri;
            } else {
                // No extension found - this should be directory itself - just add default file name at the end
                var uriWithAddedFile = Path.Combine(localPath, Context.Configuration.DefaultFileName);
                var ub = new UriBuilder(uri);
                ub.Path = Path.Combine(localPath, Context.Configuration.DefaultFileName);
                result = ub.Uri;
            }
            return result;
        }

        private string GetContentType(Uri uri) {
            var modifiedUri = GetModifiedUriFromRequest(uri);
            var extension = Path.GetExtension(modifiedUri.LocalPath);
            var result = GetContentTypeFromExtension(extension);
            return result;
        }

        private string GetContentTypeFromExtension(string extension) {
            string result;
            var mapItem = Context.Configuration.ExtensionToContentTypeMap.FirstOrDefault(x => x.Extension.Equals(extension, StringComparison.OrdinalIgnoreCase));
            if (mapItem != null) {
                // Extension found in MIME map
                result = mapItem.MIME;
            } else {
                // This extension was not found in MIME map
                if (Context.Configuration.ServeUnknownFileExtensions) {
                    result = "application/octet-stream";
                } else {
                    // We should not serve unknown file extensions
                    result = string.Empty;
                }
            }
            return result;
        }

        private string ToLocalPath(Uri uri) {
            string result = null;
            var prefixUri = new Uri(_modifiedPrefix);
            var prefixAbsolutePath = prefixUri.AbsolutePath.Trim('/');
            var uriAbsolutePath = uri.AbsolutePath.Trim('/');
            if (uriAbsolutePath.StartsWith(prefixAbsolutePath, StringComparison.OrdinalIgnoreCase)) {
                var ub = new UriBuilder(uri);
                ub.Scheme = Uri.UriSchemeFile;
                ub.Host = string.Empty;
                var pathWithoutBase = uriAbsolutePath.Substring(prefixAbsolutePath.Length).TrimStart('/');
                var path = Path.Combine(Path.GetFullPath(Context.Configuration.BasePath), pathWithoutBase);
                ub.Path = path;
                result = ub.Uri.LocalPath;
            }
            return result;
        }

        private Stream GetDirectoryListingDeniedStream() {
            var result = GetSimpleHtmlErrorTextStream("Directory Listing Denied");
            return result;
        }

        private Stream GetSimpleHtmlErrorTextStream(string body) {
            var result = GetStream(GetSimpleHtmlErrorText(body));
            return result;
        }

        private string GetSimpleHtmlErrorText(string body) {
            var encodedBody = WebUtility.HtmlEncode(body);
            var result = string.Format("<!DOCTYPE html><html><head><title>Error</title></head><body>{0}</body></html>", encodedBody);
            return result;
        }

        private Stream GetStream(string text) {
            var result = new MemoryStream();
            var textBytes = Encoding.UTF8.GetBytes(text);
            result.Write(textBytes, 0, textBytes.Length);
            result.Position = 0;
            return result;
        }
    }
}
